package com.jsftoolkit.gen;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;

import javax.xml.parsers.SAXParser;

import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.digester.Digester;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import com.jsftoolkit.base.ResourceInfo;
import com.jsftoolkit.gen.info.ComponentInfo;
import com.jsftoolkit.gen.info.ConfigInfo;
import com.jsftoolkit.gen.info.ConstantInfo;
import com.jsftoolkit.gen.info.PropertyInfo;
import com.jsftoolkit.gen.info.RendererInfo;
import com.jsftoolkit.gen.info.TagInfo;
import com.jsftoolkit.utils.ClassUtils;
import com.jsftoolkit.utils.Utils;

/**
 * Parses and XML file into an instance of {@link ComponentInfo}.
 * <p>
 * Use of this class requires Commons Digester (and its dependencies) be in the
 * class path.
 * <p>
 * There isn't much configuration to do. Create an instance, and call parse on
 * an input stream.
 */
public class XmlComponentInfoFactory {
	private static final String COMPONENT = "component";

	private static final String PROPERTY = COMPONENT + "/property";

	private static final String CONSTANT = COMPONENT + "/constant";

	private static final String RENDERER = COMPONENT + "/renderer";

	private static final String RESOURCE = "*/resource";

	private static final String CONFIG = COMPONENT + "/config";

	private static final String TAG = COMPONENT + "/tag";

	private transient Digester digester;

	private static final Converter oldClassConverter;

	static {
		// register a converter for class that can handle primitive types
		oldClassConverter = ConvertUtils.lookup(Class.class);
		ConvertUtils.register(new Converter() {
			@SuppressWarnings("unchecked")
			public Object convert(Class clazz, Object obj) {
				if (Class.class == clazz) {
					Class value = ClassUtils.forPrimitive(Utils.toString(obj));
					if (value != null) {
						return value;
					}
				}
				return oldClassConverter.convert(clazz, obj);
			}
		}, Class.class);
	}

	/**
	 * Configures the given digester.
	 */
	protected XmlComponentInfoFactory(Digester digester) {
		this.digester = digester;
		// only entity to resolve is our DTD, so always return that
		digester.setEntityResolver(new EntityResolver() {
			public InputSource resolveEntity(String publicId, String systemId) {
				return new InputSource(getClass().getResourceAsStream(
						"/com/jsftoolkit/gen/component-info.dtd"));
			}
		});

		// the root is a ComponentInfo instance
		digester.addObjectCreate(COMPONENT, ComponentInfo.class);
		digester.addSetProperties(COMPONENT);

		// imports and interfaces can be added to ComponentInfo,
		// RendererInfo and TagInfo.
		// should pass the body text as the argument
		digester.addCallMethod("*/import", "addImport", 0);
		digester.addCallMethod("*/interface", "addInterface", 0);

		// listeners can be added to ComponentInfo
		digester.addCallMethod(COMPONENT + "/listener", "addListener", 0);

		// Add Constants to ComponentInfo
		digester.addObjectCreate(CONSTANT, ConstantInfo.class);
		digester.addSetProperties(CONSTANT);
		digester.addSetNext(CONSTANT, "addConstant");

		// add RendererInfo to ComponentInfo
		digester.addObjectCreate(RENDERER, RendererInfo.class);
		digester.addSetProperties(RENDERER);
		digester.addSetNext(RENDERER, "setRenderer");
		digester.addSetTop(RENDERER, "setComponent");

		// add attributes to the renderer
		digester.addCallMethod(RENDERER + "/attribute", "addAttribute", 0);
		// add templates
		digester.addCallMethod(RENDERER + "/template", "addTemplate", 0);

		// add TagInfo to ComponentInfo
		digester.addObjectCreate(TAG, TagInfo.class);
		digester.addSetProperties(TAG);
		digester.addSetNext(TAG, "setTag");
		digester.addSetTop(TAG, "setComponent");

		// add ConfigInfo to ComponentInfo
		digester.addObjectCreate(CONFIG, ConfigInfo.class);
		digester.addSetProperties(CONFIG);
		digester.addSetNext(CONFIG, "setConfig");
		digester.addSetTop(CONFIG, "setComponent");

		// Add ResourceInfos to RendererInfo
		digester.addObjectCreate(RESOURCE, ResourceInfo.class);
		digester.addSetProperties(RESOURCE);
		digester.addSetNext(RESOURCE, "addInclude");

		// add PropertyInfos to ComponentInfo
		digester.addObjectCreate(PROPERTY, PropertyInfo.class);
		digester.addSetProperties(PROPERTY);
		digester.addSetNext(PROPERTY, "addProperty");
	}

	/**
	 * Parses documents with the given reader.
	 * 
	 * @param reader
	 */
	public XmlComponentInfoFactory(XMLReader reader) {
		this(new Digester(reader));
	}

	/**
	 * Parses documents with the given parser.
	 * 
	 * @param reader
	 */
	public XmlComponentInfoFactory(SAXParser reader) {
		this(new Digester(reader));
	}

	/**
	 * Creates a new instance using the default {@link XMLReader}
	 * 
	 * @throws SAXException
	 *             if there is a problem creating the {@link XMLReader}.
	 */
	public XmlComponentInfoFactory() throws SAXException {
		this(XMLReaderFactory.createXMLReader());
	}

	/**
	 * Loads from the given {@link Reader}.
	 * 
	 * @param in
	 * @return
	 * @throws IOException
	 * @throws SAXException
	 */
	public ComponentInfo parse(Reader in) throws IOException, SAXException {
		return (ComponentInfo) digester.parse(in);
	}

	/**
	 * 
	 * @param in
	 * @return
	 * @throws IOException
	 * @throws SAXException
	 */
	public ComponentInfo parse(InputSource in) throws IOException, SAXException {
		return (ComponentInfo) digester.parse(in);
	}

	/**
	 * 
	 * @param in
	 * @return
	 * @throws IOException
	 * @throws SAXException
	 */
	public ComponentInfo parse(InputStream in) throws IOException, SAXException {
		return (ComponentInfo) digester.parse(in);

	}
}
